Playwright 自动登录实战:CDP 与 Camoufox 两条路线对比
Playwright 自动登录实战:CDP 与 Camoufox 两条路线对比
同样是用 Playwright 自动登录一个网站,走 CDP 连 Chrome 和走 Camoufox 连 Firefox,从代码到效果完全是两个世界。
一、两条路线概览
| 维度 | CDP 路线 | Camoufox 路线 |
|---|---|---|
| 浏览器 | Chromium / Chrome | Camoufox(Firefox 魔改) |
| 协议 | Chrome DevTools Protocol | Juggler(Playwright 原生 Firefox 协议) |
| 反检测 | 无,需要自己加 stealth 补丁 | C++ 级内置,开箱即用 |
| 指纹伪造 | JavaScript 注入,可被检测 | 引擎层修改,JS 看到的就是"真实"值 |
| 适用场景 | 内部系统、无反爬的网站 | Google、Cloudflare、有反机器人检测的网站 |
二、路线 A:CDP 连接 Chrome
这是最常见的方案。启动一个 Chrome 实例,通过 CDP 端口连接。
基本流程
from playwright.async_api import async_playwright
import asyncio
async def login_via_cdp():
async with async_playwright() as p:
# 连接到已运行的 Chrome(通过 CDP)
browser = await p.chromium.connect_over_cdp("http://localhost:9222")
context = browser.contexts[0]
page = context.pages[0] if context.pages else await context.new_page()
await page.goto("https://example.com/login")
await page.fill('input[name="username"]', '<username>')
await page.fill('input[name="password"]', '<password>')
await page.click('button[type="submit"]')
await page.wait_for_url("**/dashboard**")
# 保存登录状态
storage = await context.storage_state(path="state.json")
print("登录成功,状态已保存")
await browser.close()
asyncio.run(login_via_cdp())
启动 Chrome 的方式
# 方式一:直接启动带 CDP 的 Chrome
google-chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-profile \
--no-first-run
# 方式二:Docker(BrowserStation 场景)
docker run -d -p 9222:9222 zenika/alpine-chrome:100 \
--no-sandbox \
--remote-debugging-address=0.0.0.0 \
--remote-debugging-port=9222
或者让 Playwright 自己启动
async def login_local_chrome():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
context = await browser.new_context()
page = await context.new_page()
await page.goto("https://example.com/login")
await page.fill('input[name="username"]', '<username>')
await page.fill('input[name="password"]', '<password>')
await page.click('button[type="submit"]')
await page.wait_for_url("**/dashboard**")
await context.storage_state(path="state.json")
await browser.close()
CDP 路线的问题
在没有反爬的内部系统上,上面的代码跑得很好。但一旦目标网站有反机器人检测,你会遇到:
1. navigator.webdriver 泄露
// 反爬脚本检测
if (navigator.webdriver === true) {
// 你是机器人
}
Playwright 启动的 Chromium 默认 navigator.webdriver = true。
2. CDP 协议本身可被检测
反爬系统可以通过多种方式检测 CDP 的存在:
- 检查
Runtime.evaluate注入的全局变量 - 检测
window.__playwright__binding__等 Playwright 注入的对象 - 通过
Object.getOwnPropertyDescriptor发现被覆写的属性 - 对比
toString()返回值是否还是[native code]
3. 指纹不一致
即使你用 stealth 插件伪造了 User-Agent,但 Canvas、WebGL、AudioContext 等深层指纹没变,反爬系统一做交叉验证就露馅了。
加 Stealth 补丁(治标不治本)
# pip install playwright-stealth
from playwright_stealth import stealth_async
async def login_with_stealth():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
context = await browser.new_context()
page = await context.new_page()
# 注入 stealth 脚本
await stealth_async(page)
await page.goto("https://example.com/login")
# ... 登录逻辑同上
Stealth 插件做的事情:
- 把
navigator.webdriver设为undefined - 伪造
window.chrome对象 - 修改
navigator.permissions.query的行为 - 隐藏 WebDriver 相关的 CDP 变量
但这些都是 JavaScript 层面的伪造,反爬系统可以用更深层的检测手段识破:
// 反爬系统的反制
// 检查属性描述符是否被篡改
const desc = Object.getOwnPropertyDescriptor(navigator, 'webdriver');
if (desc && desc.get) {
// 原生属性不应该有自定义 getter → 你在伪造
}
// 检查 Worker 线程里的值(stealth 通常只改主线程)
const worker = new Worker(URL.createObjectURL(
new Blob([`postMessage(navigator.webdriver)`])
));
worker.onmessage = (e) => {
if (e.data === true) {
// Worker 里没被改 → 你在伪造
}
};
三、路线 B:Camoufox
Camoufox 是一个修改了 C++ 源码的 Firefox 分支。它的 Python 库直接返回 Playwright 的 Browser 对象,你现有的 Playwright 代码几乎不用改。
安装
# 安装 Camoufox(含 GeoIP 支持)
pip install -U "camoufox[geoip]"
# 首次运行会自动下载 Camoufox 浏览器二进制文件
python -c "from camoufox.sync_api import Camoufox; Camoufox()"
基本登录
from camoufox.async_api import AsyncCamoufox
import asyncio
async def login_via_camoufox():
async with AsyncCamoufox(
headless=False,
humanize=True, # 贝塞尔曲线鼠标移动
os="windows", # 伪装成 Windows 用户
) as browser:
page = await browser.new_page()
await page.goto("https://example.com/login")
await page.fill('input[name="username"]', '<username>')
await page.fill('input[name="password"]', '<password>')
await page.click('button[type="submit"]')
await page.wait_for_url("**/dashboard**")
# 保存状态(跟 Playwright 完全一样的 API)
context = browser.contexts[0]
await context.storage_state(path="state.json")
print("登录成功")
asyncio.run(login_via_camoufox())
注意看——登录逻辑的代码跟 CDP 路线几乎一模一样。区别只在浏览器初始化部分。
带代理 + GeoIP 自动匹配
async def login_with_proxy():
async with AsyncCamoufox(
headless=True,
humanize=True,
geoip=True, # 自动根据代理 IP 设置时区、语言、地理位置
proxy={
"server": "http://proxy.example.com:8080",
"username": "<proxy_user>",
"password": "<proxy_pass>",
},
) as browser:
page = await browser.new_page()
await page.goto("https://example.com/login")
# ... 登录逻辑
geoip=True 做了什么:
- 自动检测代理出口 IP 的地理位置
- 设置匹配的
timezone、locale、geolocation - 伪造 WebRTC IP 地址
- 根据目标地区的语言分布随机生成浏览器语言
这意味着你用日本代理,浏览器的时区就是 Asia/Tokyo,语言可能是 ja-JP,WebRTC 不会泄露真实 IP。全部自动完成,不需要手动配置。
过 Cloudflare Turnstile
async def login_behind_cloudflare():
async with AsyncCamoufox(
headless=False,
humanize=True,
disable_coop=True, # 允许点击 cross-origin iframe 里的 Turnstile
) as browser:
page = await browser.new_page()
await page.goto("https://example.com/login")
# Cloudflare Turnstile 验证
await page.wait_for_load_state("networkidle")
await page.wait_for_timeout(3000)
# Turnstile 通常在 iframe 里,Camoufox 的人性化鼠标可以直接点
# 具体坐标需要根据页面布局调整
turnstile_frame = page.frame_locator("iframe[src*='challenges.cloudflare.com']")
await turnstile_frame.locator("input[type='checkbox']").click()
await page.wait_for_timeout(3000)
# 继续登录
await page.fill('input[name="username"]', '<username>')
await page.fill('input[name="password"]', '<password>')
await page.click('button[type="submit"]')
复用登录状态
import os
STATE_FILE = "login_state.json"
async def login_once_reuse_forever():
if os.path.exists(STATE_FILE):
# 复用已有状态
async with AsyncCamoufox(headless=True) as browser:
context = await browser.new_context(storage_state=STATE_FILE)
page = await context.new_page()
await page.goto("https://example.com/dashboard")
if "/login" in page.url:
print("状态过期,重新登录")
os.remove(STATE_FILE)
return await login_once_reuse_forever()
print("复用登录状态成功")
return page
else:
# 首次登录
async with AsyncCamoufox(headless=False, humanize=True) as browser:
page = await browser.new_page()
await page.goto("https://example.com/login")
await page.fill('input[name="username"]', '<username>')
await page.fill('input[name="password"]', '<password>')
await page.click('button[type="submit"]')
await page.wait_for_url("**/dashboard**")
context = browser.contexts[0]
await context.storage_state(path=STATE_FILE)
print("首次登录完成,状态已保存")
四、深层对比
反检测能力
| 检测维度 | CDP + Chrome | CDP + Stealth | Camoufox |
|---|---|---|---|
navigator.webdriver |
❌ 暴露 | ⚠️ JS 伪造,可被反制 | ✅ 引擎层返回 undefined |
| CDP 协议痕迹 | ❌ 可检测 | ⚠️ 部分隐藏 | ✅ 不使用 CDP,用 Juggler |
| Playwright 注入变量 | ❌ 可检测 | ⚠️ 部分隐藏 | ✅ 沙箱隔离,页面看不到 |
| Canvas 指纹 | ❌ 真实值暴露 | ⚠️ 噪声注入,可检测注入行为 | ✅ C++ 层伪造 |
| WebGL 指纹 | ❌ 真实值暴露 | ⚠️ 部分伪造 | ✅ C++ 层伪造 |
| AudioContext | ❌ 真实值暴露 | ❌ 通常不处理 | ✅ C++ 层伪造 |
| 字体枚举 | ❌ 真实值暴露 | ❌ 通常不处理 | ✅ 伪造 + 匹配目标 OS |
| WebRTC IP 泄露 | ❌ 泄露真实 IP | ⚠️ 可禁用但行为异常 | ✅ 协议层伪造 |
| TLS 指纹 (JA3/JA4) | ⚠️ Chromium 特征 | ⚠️ 同左 | ✅ Firefox 特征,更难识别 |
| Worker 线程一致性 | ❌ stealth 不覆盖 Worker | ❌ 同左 | ✅ 引擎层修改,全局一致 |
toString() 检测 |
❌ 暴露 JS 覆写 | ⚠️ 部分处理 | ✅ 原生返回 [native code] |
为什么 Juggler 比 CDP 更隐蔽
CDP 的问题在于它是 Chromium 的一部分,反爬系统对它研究得非常透彻。常见的检测方式:
// 检测 CDP 的 Runtime.evaluate 痕迹
// CDP 执行 JS 时会在调用栈里留下特征
try {
null[0]();
} catch (e) {
if (e.stack.includes('Runtime.evaluate')) {
// CDP 在控制这个浏览器
}
}
Juggler 是 Playwright 团队为 Firefox 开发的独立协议,不是 Firefox 核心的一部分。Camoufox 进一步修改了 Juggler:
- Playwright 的所有 Page Agent JS 运行在隔离的沙箱世界中
- 页面无法通过 getter 劫持检测到属性被读取
- 输入事件直接走 Firefox 原生的用户输入处理器,跟真人操作的代码路径完全一致
指纹一致性
CDP + Stealth 最大的问题不是单个参数伪造不了,而是参数之间不自洽:
❌ 典型的 Stealth 指纹矛盾:
User-Agent: Windows 10 / Chrome 120
WebGL Renderer: Apple M1 GPU ← 不可能出现在 Windows 上
屏幕分辨率: 2560x1600 ← MacBook 分辨率
字体列表: 包含 SF Pro ← macOS 独有字体
Camoufox 使用 BrowserForge 生成指纹,确保所有参数匹配真实设备的统计分布:
✅ Camoufox 生成的一致指纹:
User-Agent: Windows 10 / Firefox 135
WebGL Renderer: NVIDIA GeForce RTX 3060
屏幕分辨率: 1920x1080
字体列表: Segoe UI, Calibri, Consolas ← 全是 Windows 字体
时区: America/Chicago ← 匹配代理 IP 地理位置
语言: en-US
五、性能与资源对比
| 指标 | CDP + Chrome | Camoufox |
|---|---|---|
| 启动时间 | ~1-2s | ~2-3s(首次需下载二进制 ~300MB) |
| 内存占用 | ~150-300MB | ~200-400MB |
| 页面加载速度 | 快(V8 引擎优化好) | 略慢(Firefox 引擎) |
| Chromium 专属网站兼容性 | ✅ 完美 | ⚠️ 极少数 Chrome-only 功能不支持 |
| 并发能力 | 高 | 中等(指纹生成有开销) |
六、怎么选
用 CDP + Chrome 的场景:
- 目标是内部系统、无反爬的网站
- 需要 Chrome 特有的 API 或功能
- 已有大量 CDP 基础设施(如 BrowserStation)
- 追求最大并发和最低资源占用
用 Camoufox 的场景:
- 目标网站有 Cloudflare、Akamai、PerimeterX 等反机器人系统
- 需要登录 Google、LinkedIn、Amazon 等大厂网站
- 需要多账号隔离,每个账号要有独立且一致的指纹
- 需要代理 + 地理位置自动匹配
混合使用(推荐):
async def smart_login(url: str, has_antibot: bool = False):
if has_antibot:
async with AsyncCamoufox(humanize=True, geoip=True, proxy=PROXY) as browser:
page = await browser.new_page()
await page.goto(url)
return page
else:
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp("http://browserstation:9222")
page = browser.contexts[0].pages[0]
await page.goto(url)
return page
七、总结
CDP 和 Camoufox 不是非此即彼的关系。CDP 是基础设施级的浏览器控制协议,成熟、快速、生态完善;Camoufox 是专门解决"被检测"这个问题的工具,在反爬对抗上做到了目前开源方案的天花板。
核心区别就一句话:CDP + Stealth 是在 JavaScript 层面"演戏",Camoufox 是在 C++ 层面"整容"。 演戏可以被拆穿,整容后连 X 光都看不出来。
选择哪条路线,取决于你的对手是谁。